*
* If called with no arguments, all values will be returned.
*
- * @param selection mixed String key or array of keys to get values for.
- * @param fallback mixed Value to use in case key(s) do not exist (optional).
+ * @param {string|Array} selection String key or array of keys to get values for.
+ * @param {Mixed} [fallback] Value to use in case key(s) do not exist.
* @return mixed If selection was a string returns the value or null,
* If selection was an array, returns an object of key/values (value is null if not found),
* If selection was not passed or invalid, will return the 'values' object member (be careful as
/**
* Sets one or multiple key/value pairs.
*
- * @param selection {mixed} String key or array of keys to set values for.
- * @param value {mixed} Value to set (optional, only in use when key is a string)
+ * @param {string|Object} selection String key to set value for, or object mapping keys to values.
+ * @param {Mixed} [value] Value to set (optional, only in use when key is a string)
* @return {Boolean} This returns true on success, false on failure.
*/
set: function ( selection, value ) {
/**
* Checks if one or multiple keys exist.
*
- * @param selection {mixed} String key or array of keys to check
+ * @param {Mixed} selection String key or array of keys to check
* @return {boolean} Existence of key(s)
*/
exists: function ( selection ) {
Message.prototype = {
/**
- * Simple message parser, does $N replacement, HTML-escaping (only for
- * 'escaped' format), and nothing else.
+ * Simple message parser, does $N replacement and nothing else.
*
* This may be overridden to provide a more complex message parser.
*
* 'dependencies': ['required.foo', 'bar.also', ...], (or) function () {}
* 'group': 'somegroup', (or) null,
* 'source': 'local', 'someforeignwiki', (or) null
- * 'state': 'registered', 'loading', 'loaded', 'ready', 'error' or 'missing'
+ * 'state': 'registered', 'loaded', 'loading', 'ready', 'error' or 'missing'
* 'script': ...,
* 'style': ...,
* 'messages': { 'key': 'value' },
// Selector cache for the marker element. Use getMarker() to get/use the marker!
$marker = null,
// Buffer for addEmbeddedCSS.
- cssBuffer = '';
+ cssBuffer = '',
+ // Callbacks for addEmbeddedCSS.
+ cssCallbacks = $.Callbacks();
/* Private methods */
/**
* @param {string} [cssText=cssBuffer] If called without cssText,
* the internal buffer will be inserted instead.
+ * @param {Function} callback
*/
- function addEmbeddedCSS( cssText ) {
+ function addEmbeddedCSS( cssText, callback ) {
var $style, styleEl;
+ if ( callback ) {
+ cssCallbacks.add( callback );
+ }
+
// Yield once before inserting the <style> tag. There are likely
// more calls coming up which we can combine this way.
// Appending a stylesheet and waiting for the browser to repaint
} else {
styleEl.appendChild( document.createTextNode( String( cssText ) ) );
}
+ cssCallbacks.fire().empty();
return;
}
}
$( addStyleTag( cssText, getMarker() ) ).data( 'ResourceLoaderDynamicStyleTag', true );
+
+ cssCallbacks.fire().empty();
}
/**
*
* @private
* @param {string|string[]} states Module states to filter by
- * @param {Array} modules List of module names to filter (optional, by default the entire
+ * @param {Array} [modules] List of module names to filter (optional, by default the entire
* registry is used)
* @return {Array} List of filtered module names
*/
* @param {string} module Module name to execute
*/
function execute( module ) {
- var key, value, media, i, urls, script, markModuleReady, nestedAddScript;
+ var key, value, media, i, urls, cssHandle, checkCssHandles,
+ cssHandlesRegistered = false;
if ( registry[module] === undefined ) {
throw new Error( 'Module has not been registered yet: ' + module );
} else if ( registry[module].state === 'loading' ) {
throw new Error( 'Module has not completed loading yet: ' + module );
} else if ( registry[module].state === 'ready' ) {
- throw new Error( 'Module has already been loaded: ' + module );
+ throw new Error( 'Module has already been executed: ' + module );
}
/**
el.href = url;
}
+ function runScript() {
+ var script, markModuleReady, nestedAddScript;
+ try {
+ script = registry[module].script;
+ markModuleReady = function () {
+ registry[module].state = 'ready';
+ handlePending( module );
+ };
+ nestedAddScript = function ( arr, callback, async, i ) {
+ // Recursively call addScript() in its own callback
+ // for each element of arr.
+ if ( i >= arr.length ) {
+ // We're at the end of the array
+ callback();
+ return;
+ }
+
+ addScript( arr[i], function () {
+ nestedAddScript( arr, callback, async, i + 1 );
+ }, async );
+ };
+
+ if ( $.isArray( script ) ) {
+ nestedAddScript( script, markModuleReady, registry[module].async, 0 );
+ } else if ( $.isFunction( script ) ) {
+ registry[module].state = 'ready';
+ script( $ );
+ handlePending( module );
+ }
+ } catch ( e ) {
+ // This needs to NOT use mw.log because these errors are common in production mode
+ // and not in debug mode, such as when a symbol that should be global isn't exported
+ log( 'Exception thrown by ' + module + ': ' + e.message, e );
+ registry[module].state = 'error';
+ handlePending( module );
+ }
+ }
+
+ // This used to be inside runScript, but since that is now fired asychronously
+ // (after CSS is loaded) we need to set it here right away. It is crucial that
+ // when execute() is called this is set synchronously, otherwise modules will get
+ // executed multiple times as the registry will state that it isn't loading yet.
+ registry[module].state = 'loading';
+
+ // Add localizations to message system
+ if ( $.isPlainObject( registry[module].messages ) ) {
+ mw.messages.set( registry[module].messages );
+ }
+
+ // Make sure we don't run the scripts until all (potentially asynchronous)
+ // stylesheet insertions have completed.
+ ( function () {
+ var pending = 0;
+ checkCssHandles = function () {
+ // cssHandlesRegistered ensures we don't take off too soon, e.g. when
+ // one of the cssHandles is fired while we're still creating more handles.
+ if ( cssHandlesRegistered && pending === 0 && runScript ) {
+ runScript();
+ runScript = undefined; // Revoke
+ }
+ };
+ cssHandle = function () {
+ var check = checkCssHandles;
+ pending++;
+ return function () {
+ if (check) {
+ pending--;
+ check();
+ check = undefined; // Revoke
+ }
+ };
+ };
+ }() );
+
// Process styles (see also mw.loader.implement)
// * back-compat: { <media>: css }
// * back-compat: { <media>: [url, ..] }
// Strings are pre-wrapped in "@media". The media-type was just ""
// (because it had to be set to something).
// This is one of the reasons why this format is no longer used.
- addEmbeddedCSS( value );
+ addEmbeddedCSS( value, cssHandle() );
} else {
// back-compat: { <media>: [url, ..] }
media = key;
addLink( media, value[i] );
} else if ( key === 'css' ) {
// { "css": [css, ..] }
- addEmbeddedCSS( value[i] );
+ addEmbeddedCSS( value[i], cssHandle() );
}
}
// Not an array, but a regular object
}
}
- // Add localizations to message system
- if ( $.isPlainObject( registry[module].messages ) ) {
- mw.messages.set( registry[module].messages );
- }
-
- // Execute script
- try {
- script = registry[module].script;
- markModuleReady = function () {
- registry[module].state = 'ready';
- handlePending( module );
- };
- nestedAddScript = function ( arr, callback, async, i ) {
- // Recursively call addScript() in its own callback
- // for each element of arr.
- if ( i >= arr.length ) {
- // We're at the end of the array
- callback();
- return;
- }
-
- addScript( arr[i], function () {
- nestedAddScript( arr, callback, async, i + 1 );
- }, async );
- };
-
- if ( $.isArray( script ) ) {
- registry[module].state = 'loading';
- nestedAddScript( script, markModuleReady, registry[module].async, 0 );
- } else if ( $.isFunction( script ) ) {
- registry[module].state = 'ready';
- script( $ );
- handlePending( module );
- }
- } catch ( e ) {
- // This needs to NOT use mw.log because these errors are common in production mode
- // and not in debug mode, such as when a symbol that should be global isn't exported
- log( 'Exception thrown by ' + module + ': ' + e.message, e );
- registry[module].state = 'error';
- handlePending( module );
- }
+ // Kick off.
+ cssHandlesRegistered = true;
+ checkCssHandles();
}
/**
* Registers a module, letting the system know about it and its
* properties. Startup modules contain calls to this function.
*
- * @param module {String}: Module name
- * @param version {Number}: Module version number as a timestamp (falls backs to 0)
- * @param dependencies {String|Array|Function}: One string or array of strings of module
+ * @param {string} module Module name
+ * @param {number} version Module version number as a timestamp (falls backs to 0)
+ * @param {string|Array|Function} dependencies One string or array of strings of module
* names on which this module depends, or a function that returns that array.
- * @param group {String}: Group which the module is in (optional, defaults to null)
- * @param source {String}: Name of the source. Defaults to local.
+ * @param {string} [group=null] Group which the module is in
+ * @param {string} [source='local'] Name of the source
*/
register: function ( module, version, dependencies, group, source ) {
var m;
/**
* Executes a function as soon as one or more required modules are ready
*
- * @param dependencies {String|Array} Module name or array of modules names the callback
+ * @param {string|Array} dependencies Module name or array of modules names the callback
* dependends on to be ready before executing
- * @param ready {Function} callback to execute when all dependencies are ready (optional)
- * @param error {Function} callback to execute when if dependencies have a errors (optional)
+ * @param {Function} [ready] callback to execute when all dependencies are ready
+ * @param {Function} [error] callback to execute when if dependencies have a errors
*/
using: function ( dependencies, ready, error ) {
var tod = typeof dependencies;
/**
* Loads an external script or one or more modules for future use
*
- * @param modules {mixed} Either the name of a module, array of modules,
+ * @param {string|Array} modules Either the name of a module, array of modules,
* or a URL of an external script or style
- * @param type {String} mime-type to use if calling with a URL of an
+ * @param {string} [type='text/javascript'] mime-type to use if calling with a URL of an
* external script or style; acceptable values are "text/css" and
* "text/javascript"; if no type is provided, text/javascript is assumed.
- * @param async {Boolean} (optional) If true, load modules asynchronously
- * even if document ready has not yet occurred. If false (default),
- * block before document ready and load async after. If not set, true will
- * be assumed if loading a URL, and false will be assumed otherwise.
+ * @param {boolean} [async] If true, load modules asynchronously
+ * even if document ready has not yet occurred. If false, block before
+ * document ready and load async after. If not set, true will be
+ * assumed if loading a URL, and false will be assumed otherwise.
*/
load: function ( modules, type, async ) {
var filtered, m, module, l;
/**
* Changes the state of a module
*
- * @param module {String|Object} module name or object of module name/state pairs
- * @param state {String} state name
+ * @param {string|Object} module module name or object of module name/state pairs
+ * @param {string} state state name
*/
state: function ( module, state ) {
var m;
/**
* Gets the version of a module
*
- * @param module string name of module to get version for
+ * @param {string} module name of module to get version for
*/
getVersion: function ( module ) {
if ( registry[module] !== undefined && registry[module].version !== undefined ) {
/**
* Gets the state of a module
*
- * @param module string name of module to get state for
+ * @param {string} module name of module to get state for
*/
getState: function ( module ) {
if ( registry[module] !== undefined && registry[module].state !== undefined ) {
/**
* Create an HTML element string, with safe escaping.
*
- * @param name The tag name.
- * @param attrs An object with members mapping element names to values
- * @param contents The contents of the element. May be either:
+ * @param {string} name The tag name.
+ * @param {Object} attrs An object with members mapping element names to values
+ * @param {Mixed} contents The contents of the element. May be either:
* - string: The string is escaped.
* - null or undefined: The short closing form is used, e.g. <br/>.
* - this.Raw: The value attribute is included without escaping.
'gender-plural-msg': '{{GENDER:$1|he|she|they}} {{PLURAL:$2|is|are}} awesome',
'grammar-msg': 'Przeszukaj {{GRAMMAR:grammar_case_foo|{{SITENAME}}}}',
'formatnum-msg': '{{formatnum:$1}}',
- 'int-msg': 'Some {{int:other-message}}'
+ 'int-msg': 'Some {{int:other-message}}',
+ 'mediawiki-test-version-entrypoints-index-php': '[https://www.mediawiki.org/wiki/Manual:index.php index.php]',
+ 'external-link-replace': 'Foo [$1 bar]'
} );
- // For formatnum tests
- mw.config.set( 'wgUserLanguage', 'en' );
+ mw.config.set( {
+ wgArticlePath: '/wiki/$1',
+
+ // For formatnum tests
+ wgUserLanguage: 'en'
+ } );
specialCharactersPageName = '"Who" wants to be a millionaire & live on \'Exotic Island\'?';
}
assert.ok( mw.config instanceof mw.Map, 'mw.config instance of mw.Map' );
} );
- QUnit.test( 'mw.message & mw.messages', 54, function ( assert ) {
+ QUnit.test( 'mw.message & mw.messages', 68, function ( assert ) {
var goodbye, hello;
// Convenience method for asserting the same result for multiple formats
assert.equal( hello.escaped(), 'Hello <b>awesome</b> world', 'Message.escaped returns the escaped message' );
assert.equal( hello.format, 'escaped', 'Message.escaped correctly updated the "format" property' );
- assert.ok( mw.messages.set( 'escaped-with-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ) );
- assert.equal( mw.message( 'escaped-with-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName' ) + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+ assert.ok( mw.messages.set( 'multiple-curly-brace', '"{{SITENAME}}" is the home of {{int:other-message}}' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['multiple-curly-brace'], ['text', 'parse'], '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message', 'Curly brace format works correctly' );
+ assert.equal( mw.message( 'multiple-curly-brace' ).plain(), mw.messages.get( 'multiple-curly-brace' ), 'Plain format works correctly for curly brace message' );
+ assert.equal( mw.message( 'multiple-curly-brace' ).escaped(), mw.html.escape( '"' + mw.config.get( 'wgSiteName') + '" is the home of Other Message' ), 'Escaped format works correctly for curly brace message' );
+
+ assert.ok( mw.messages.set( 'multiple-square-brackets-and-ampersand', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ), 'mw.messages.set: Register' );
+ assertMultipleFormats( ['multiple-square-brackets-and-ampersand'], ['plain', 'text'], mw.messages.get( 'multiple-square-brackets-and-ampersand' ), 'Square bracket message is not processed' );
+ assert.equal( mw.message( 'multiple-square-brackets-and-ampersand' ).escaped(), 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
+ assert.htmlEqual( mw.message( 'multiple-square-brackets-and-ampersand' ).parse(), 'Visit the ' +
+ '<a title="Project:Community portal" href="/wiki/Project:Community_portal">community portal</a>' +
+ ' & <a title="Project:Help desk" href="/wiki/Project:Help_desk">help desk</a>', 'Internal links work with parse' );
+
+ assertMultipleFormats( ['mediawiki-test-version-entrypoints-index-php'], ['plain', 'text', 'escaped'], mw.messages.get( 'mediawiki-test-version-entrypoints-index-php' ), 'External link markup is unprocessed' );
+ assert.htmlEqual( mw.message( 'mediawiki-test-version-entrypoints-index-php' ).parse(), '<a href="https://www.mediawiki.org/wiki/Manual:index.php">index.php</a>', 'External link works correctly in parse mode' );
- assert.ok( mw.messages.set( 'escaped-with-square-brackets', 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]' ) );
- assert.equal( mw.message( 'escaped-with-square-brackets' ).escaped(), 'Visit the [[Project:Community portal|community portal]] & [[Project:Help desk|help desk]]', 'Escaped format works correctly for square bracket message' );
+ assertMultipleFormats( ['external-link-replace', 'http://example.org/?x=y&z'], ['plain', 'text'] , 'Foo [http://example.org/?x=y&z bar]', 'Parameters are substituted but external link is not processed' );
+ assert.equal( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).escaped(), 'Foo [http://example.org/?x=y&z bar]', 'In escaped mode, parameters are substituted and ampersand is escaped, but external link is not processed' );
+ assert.htmlEqual( mw.message( 'external-link-replace', 'http://example.org/?x=y&z' ).parse(), 'Foo <a href="http://example.org/?x=y&z">bar</a>', 'External link with replacement works in parse mode without double-escaping' );
hello.parse();
assert.equal( hello.format, 'parse', 'Message.parse correctly updated the "format" property' );
assert.ok( mw.messages.set( 'mediawiki-test-categorytree-collapse-bullet', '[<b>−</b>]' ), 'mw.messages.set: Register' );
assert.equal( mw.message( 'mediawiki-test-categorytree-collapse-bullet' ).plain(), mw.messages.get( 'mediawiki-test-categorytree-collapse-bullet' ), 'Single square brackets unchanged in plain mode' );
- assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ) );
+ assert.ok( mw.messages.set( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result', '<a href=\'#\' title=\'{{#special:mypage}}\'>Username</a> (<a href=\'#\' title=\'{{#special:mytalk}}\'>talk</a>)' ), 'mw.messages.set: Register' );
assert.equal( mw.message( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ).plain(), mw.messages.get( 'mediawiki-test-wikieditor-toolbar-help-content-signature-result' ), 'HTML message with curly braces is not changed in plain mode' );
assertMultipleFormats( ['gender-plural-msg', 'male', 1], ['text', 'parse', 'escaped'], 'he is awesome', 'Gender and plural are resolved' );
assert.equal( mw.msg( 'hello' ), 'Hello <b>awesome</b> world', 'Gets message with default options (existing message)' );
assert.equal( mw.msg( 'goodbye' ), '<goodbye>', 'Gets message with default options (nonexistent message)' );
- assert.ok( mw.messages.set( 'plural-item', 'Found $1 {{PLURAL:$1|item|items}}' ) );
+ assert.ok( mw.messages.set( 'plural-item' , 'Found $1 {{PLURAL:$1|item|items}}' ), 'mw.messages.set: Register' );
assert.equal( mw.msg( 'plural-item', 5 ), 'Found 5 items', 'Apply plural for count 5' );
assert.equal( mw.msg( 'plural-item', 0 ), 'Found 0 items', 'Apply plural for count 0' );
assert.equal( mw.msg( 'plural-item', 1 ), 'Found 1 item', 'Apply plural for count 1' );
function assertStyleAsync( assert, $element, prop, val, fn ) {
var styleTestStart,
el = $element.get( 0 ),
- styleTestTimeout = ( QUnit.config.testTimeout - 200 ) || 5000;
+ styleTestTimeout = ( QUnit.config.testTimeout || 5000 ) - 200;
function isCssImportApplied() {
// Trigger reflow, repaint, redraw, whatever (cross-browser)
} );
} );
- QUnit.test( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
+ QUnit.asyncTest( 'mw.loader.implement( styles={ "css": [text, ..] } )', 2, function ( assert ) {
var $element = $( '<div class="mw-test-implement-a"></div>' ).appendTo( '#qunit-fixture' );
assert.notEqual(
mw.loader.implement(
'test.implement.a',
function () {
- QUnit.stop();
- setTimeout(function () {
- assert.equal(
- $element.css( 'float' ),
- 'right',
- 'style is applied'
- );
- QUnit.start();
- });
+ assert.equal(
+ $element.css( 'float' ),
+ 'right',
+ 'style is applied'
+ );
+ QUnit.start();
},
{
'all': '.mw-test-implement-a { float: right; }'
] );
} );
- // Backwards compatibility
- QUnit.test( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
+ // Backwards compatibility
+ QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: text } ) (back-compat)', 2, function ( assert ) {
var $element = $( '<div class="mw-test-implement-c"></div>' ).appendTo( '#qunit-fixture' );
assert.notEqual(
mw.loader.implement(
'test.implement.c',
function () {
- QUnit.stop();
- setTimeout(function () {
- assert.equal(
- $element.css( 'float' ),
- 'right',
- 'style is applied'
- );
- QUnit.start();
- });
+ assert.equal(
+ $element.css( 'float' ),
+ 'right',
+ 'style is applied'
+ );
+ QUnit.start();
},
{
'all': '.mw-test-implement-c { float: right; }'
] );
} );
- // Backwards compatibility
+ // Backwards compatibility
QUnit.asyncTest( 'mw.loader.implement( styles={ <media>: [url, ..] } ) (back-compat)', 4, function ( assert ) {
var $element = $( '<div class="mw-test-implement-d"></div>' ).appendTo( '#qunit-fixture' ),
$element2 = $( '<div class="mw-test-implement-d2"></div>' ).appendTo( '#qunit-fixture' );
] );
} );
- // @import (bug 31676)
+ // @import (bug 31676)
QUnit.asyncTest( 'mw.loader.implement( styles has @import)', 5, function ( assert ) {
var isJsExecuted, $element;